package com.googlecode.alogwe.sprockit.js;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.WrappedException;

public class JSFile extends ScriptableObject {

	private final static OpenOption	APPEND			= StandardOpenOption.APPEND;
	private final static OpenOption	WRITE			= StandardOpenOption.WRITE;
	private final static OpenOption	CREATE			= StandardOpenOption.CREATE;
	private static final Path		BASE_FILE_PATH	= Paths.get("").toAbsolutePath();
	private static final String		JS_CLASSNAME	= "File";
	private File					file;
	private String					path;

	/**
	 * The zero-parameter constructor, required by Rhino for all host objects
	 * 
	 * When Context.defineClass is called with this class, it will construct
	 * File.prototype using this constructor.
	 */
	public JSFile() {
	}

	/**
	 * The main constructor.
	 * 
	 * When Context.defineClass is called with this class, it will construct
	 * File.prototype using this constructor.
	 * <p>
	 * <b>TODO</b> More checking to make sure the file is valid (filetype etc)
	 * 
	 * @throws InvalidPathException
	 */
	public JSFile(String filepath) throws InvalidPathException {
		if (!pathIsAllowed(filepath)) {
			throw new InvalidPathException(filepath,
					"File must be within the local application folder or its subfolders.");
		}
		this.file = new File(filepath);
		this.path = filepath;
	}

	/**
	 * The Java method defining the JavaScript 'File' constructor.
	 * 
	 * If the constructor has one or more arguments, and the first argument is
	 * not undefined, the argument is converted to a string as used as the
	 * filename.
	 * <p>
	 * 
	 * @throws InstantiationException
	 */
	public static Scriptable jsConstructor(Context cx, Object[] args, Function ctorObj,
			boolean inNewExpr) throws InstantiationException {

		if (args.length == 0 || args[0] == Context.getUndefinedValue()) {
			throw new InstantiationException("A path String required to instantiate a File object");
		}
		String filepath = Context.toString(args[0]);
		return new JSFile(filepath);
	}

	/**
	 * Returns the name of this JavaScript class. Typically the same name as the
	 * constructor.
	 */
	@Override
	public String getClassName() {
		return JS_CLASSNAME;
	}

	/**
	 * Creates it's own JavaScript host object in the given context/scope
	 * <p>
	 * <b>TODO</b> Figure out a better way to do this.
	 * 
	 * @param cx
	 *            The Context used to create the JavaScript host object
	 * @param scope
	 *            The object in the JavaScript environment that will receive
	 *            this newly created host object
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws InvocationTargetException
	 */
	public static void createJsHostObject(Context cx, Scriptable scope)
			throws IllegalAccessException, InstantiationException, InvocationTargetException {
		ScriptableObject.defineClass(scope, JSFile.class);
	}

	/**
	 * Checks that the JavaScript object being passed to it is an instance of
	 * this class (JSFile)
	 * 
	 * This is necessary to verify that a JavaScript instance prototype hasn't
	 * been modified to call methods or properties of an invalid type;
	 * <p>
	 * <code>
	 * var a = {};<br>
	 * a.__proto__ = File.prototype;<br>
	 * a.append("This will throw an Exception");<br>
	 * </code>
	 * 
	 * @param thisObj
	 *            - The object from the JavaScript environment that is being
	 *            accessed from a host method
	 * @return The object if it's an instance of this JSFile class ('File'
	 *         object in JavaScript)
	 */
	private static JSFile checkJsInstance(Scriptable thisObj) {
		if (thisObj == null || !(thisObj instanceof JSFile)) {
			throw Context.throwAsScriptRuntimeEx(new Exception(
					"Method called on incompatible type!"));
		}
		return (JSFile) thisObj;
	}

	/**
	 * Since we are only supporting text-based files (for now?) it makes sense
	 * to try to set the default Charset as Unicode UCS4 Try to get the Charset
	 * specified <code>alias</code> if the Charset is not supported then return
	 * the default Charset.
	 * <p>
	 * <b>TODO</b>
	 * <p>
	 * 
	 * @param alias
	 *            - the name of the Charset to get
	 * @return A Charset instance
	 */
	private static Charset getCharset(String alias) {
		Charset cs;
		if (Charset.isSupported(alias)) {
			cs = Charset.forName(alias);
		} else {
			cs = Charset.defaultCharset();
		}
		return cs;
	}

	/**
	 * Returns whether or not path access should be allowed. Paths that are not
	 * in the application folder ({@link #BASE_FILE_PATH}) or it's children are
	 * not allowed.
	 * <p>
	 * <b>TODO</b> Make sure symbolic links can't be created or followed to
	 * access other parts of the local file-system. (Though a SecurityManager
	 * would probably take care of that?)
	 * <p>
	 * <b>TODO</b> Look into implementing SecurityManagers or the like
	 * <p>
	 * 
	 * @param filepath
	 *            The path that will be checked.
	 * @return true or false
	 */
	private boolean pathIsAllowed(String filepath) {
		Path userpath = Paths.get(filepath).toAbsolutePath().normalize();
		return userpath.startsWith(BASE_FILE_PATH);
	}

	/*
	 * File.path getter;
	 */
	public String jsGet_path() {
		return getPath();
	}

	public String getPath() {
		return this.path;
	}

	/**
	 * JavaScript getter for File.exists
	 * 
	 * @return <code>true</code> if this file exists and is a file,
	 *         <code>false</code> otherwise
	 */
	public boolean jsGet_exists() {
		return this.file.isFile();
	}

	/**
	 * Writes all arguments to this file by appending them to the end of the
	 * file. Each argument's toString() method is called prior to writing to
	 * this file.
	 * <p>
	 * <b>NOTE</b> This method doesn't call {@link JSFile#append()} because this
	 * method has to be static to satisfy Rhino variable arguments call format.
	 * <p>
	 * <b>TODO</b> Test this a lot!
	 * <p>
	 * <b>TODO</b> Consider making a public static class method for writing to
	 * files in different ways to cut down on redundancy
	 * 
	 * @param cx
	 *            - The Context
	 * @param thisObj
	 *            - The File instance <code>this</code>
	 * @param args
	 *            - an Object[] of all the arguments from the JavaScript
	 *            function call
	 * @param funObj
	 *            - the JavaScript Function object
	 */
	public static void jsFunction_append(Context cx, Scriptable thisObj, Object[] args,
			Function funObj) {
		try {

			// Make sure we have the right type if instance
			JSFile thisFile = JSFile.checkJsInstance(thisObj);

			Path path = thisFile.file.toPath();
			Charset cs = Charset.defaultCharset();
			BufferedWriter writer = Files.newBufferedWriter(path, cs, CREATE, APPEND);

			StringBuilder output = new StringBuilder();
			for (int i = 0; i < args.length; i++) {
				output.append(Context.toString(args[i]));
			}

			writer.write(output.toString());
			writer.close();

		} catch (IOException e) {
			throw Context.throwAsScriptRuntimeEx(e);
		}
	}

	public void append(String... args) throws IOException {
		Path path = this.file.toPath();
		Charset cs = Charset.defaultCharset();
		BufferedWriter writer = Files.newBufferedWriter(path, cs, CREATE, APPEND);

		StringBuilder output = new StringBuilder();
		for (String s : args) {
			output.append(s);
		}

		writer.write(output.toString());
		writer.close();
	}

	public void append(Collection<Object> obj) throws IOException {
		Path path = this.file.toPath();
		Charset cs = Charset.defaultCharset();
		BufferedWriter writer = Files.newBufferedWriter(path, cs, CREATE, WRITE);

		StringBuilder output = new StringBuilder();
		for (Object o : obj) {
			output.append(o.toString());
		}

		writer.write(output.toString());
		writer.close();
	}

	/**
	 * JavaScript host method that reads lines from the File and returns them in
	 * an array.
	 * <p>
	 * 
	 * @throws WrappedException
	 *             from {@link JSFile#read()}
	 * @return The array of lines that were read
	 */
	public Object jsFunction_read() {

		try {
			ArrayList<String> list = this.read();
			String[] lines = list.toArray(new String[list.size()]);
			Scriptable scope = ScriptableObject.getTopLevelScope(this);
			Context cx = Context.getCurrentContext();
			return cx.newObject(scope, "Array", lines);
		} catch (IOException e) {
			// Throw into script runtime as WrappedException
			throw Context.throwAsScriptRuntimeEx(e);
		}
	}

	/**
	 * Read this file's contents into an ArrayList. Each index of the ArrayList
	 * is a separate line of the file.
	 * 
	 * @return The ArrayList of Strings
	 * @throws FileNotFoundException
	 *             if the file is not found or valid
	 *             {@link FileReader#FileReader(String)}
	 * @throws IOException
	 */
	public ArrayList<String> read() throws FileNotFoundException, IOException {
		Path p = this.file.toPath();
		Charset cs = Charset.defaultCharset();
		BufferedReader reader = Files.newBufferedReader(p, cs);

		ArrayList<String> lines = new ArrayList<String>();
		String s;
		while ((s = reader.readLine()) != null) {
			lines.add(s + "\n");
		}

		reader.close();
		return lines;
	}

	/**
	 * Writes all arguments to this file. Each argument's toString() method is
	 * called prior to writing to this file.
	 * <p>
	 * <b>NOTE</b> This method doesn't call {@link JSFile#write()} because this
	 * method has to be static to satisfy Rhino variable arguments call format.
	 * <p>
	 * <b>TODO</b> Test this.
	 * <p>
	 * <b>TODO</b> Consider making a public static class method for writing to
	 * files in different ways to cut down on redundancy
	 * 
	 * @param cx
	 *            - The Context
	 * @param thisObj
	 *            - The File instance <code>this</code>
	 * @param args
	 *            - an Object[] of all the arguments from the JavaScript
	 *            function call
	 * @param funObj
	 *            - the JavaScript Function object
	 */
	public static void jsFunction_write(Context cx, Scriptable thisObj, Object[] args,
			Function funObj) {

		try {

			// Make sure we have the right type if instance
			JSFile thisFile = JSFile.checkJsInstance(thisObj);

			Path path = thisFile.file.toPath();
			Charset cs = Charset.defaultCharset();
			BufferedWriter writer = Files.newBufferedWriter(path, cs, CREATE, WRITE);

			StringBuilder output = new StringBuilder();
			for (int i = 0; i < args.length; i++) {
				output.append(Context.toString(args[i]));
			}

			writer.write(output.toString(), 0, output.length());
			writer.close();

		} catch (IOException e) {
			throw Context.throwAsScriptRuntimeEx(e);
		}
	}

	/**
	 * Writes to this file instance.
	 * <p>
	 * <b>TODO</b> Test this.
	 * 
	 * @param output
	 *            - The String to be written
	 * @param offset
	 *            - The offset from which to start reading characters
	 * @param length
	 *            - Number of characters to be written
	 * @throws IOException
	 */
	public void write(String... args) throws IOException {
		Path p = this.file.toPath();
		Charset cs = Charset.defaultCharset();
		BufferedWriter writer = Files.newBufferedWriter(p, cs, CREATE, WRITE);

		StringBuilder output = new StringBuilder();
		for (String s : args) {
			output.append(s);
		}

		writer.write(output.toString());
		writer.close();
	}

	/**
	 * Writes a Collection of Objects to this file instance.
	 * <p>
	 * <b>TODO</b> Test this.
	 * 
	 * @param output
	 *            - The String to be written
	 * @param offset
	 *            - The offset from which to start reading characters
	 * @param length
	 *            - Number of characters to be written
	 * @throws IOException
	 */
	public void write(Collection<Object> obj) throws IOException {
		Path path = this.file.toPath();
		Charset cs = Charset.defaultCharset();
		BufferedWriter writer = Files.newBufferedWriter(path, cs, CREATE, WRITE);

		StringBuilder output = new StringBuilder();
		for (Object o : obj) {
			output.append(o.toString());
		}

		writer.write(output.toString());
		writer.close();
	}
}